package de.tu_darmstadt.gdi1.battleship.model;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
import java.util.Vector;

import de.tu_darmstadt.gdi1.battleship.exceptions.BoatFailException;
import de.tu_darmstadt.gdi1.battleship.exceptions.FieldAlreadyShotException;
import de.tu_darmstadt.gdi1.battleship.exceptions.InvalidCoordinatesException;
import de.tu_darmstadt.gdi1.battleship.exceptions.InvalidLevelFileException;
import de.tu_darmstadt.gdi1.battleship.exceptions.NoAmmoException;
import de.tu_darmstadt.gdi1.battleship.exceptions.NotUsersTurnException;
import de.tu_darmstadt.gdi1.battleship.exceptions.NumberFailException;
import de.tu_darmstadt.gdi1.battleship.view.Messages;

public class BattleshipGame {

	// The map
	private SingleMap humanMap = new SingleMap();
	private SingleMap cpuMap = new SingleMap();

	// The Opponent CPU-Player
	private CPUnew cpuBot = new CPUnew();

	// State variable, true if game is finished.
	private boolean finished = false;

	// Whether it is players (true) or CPUs (false) turn
	public static final boolean PLAYER = true;
	public static final boolean CPU = false;
	private boolean turn = PLAYER;
	private int turnNumber = 0;
	private Vector<Event> eventList = new Vector<Event>();
	private boolean ammunitionMode = false;
	public int ammo = 1;
	private String playerName = "Player";
	private boolean userGotHighscore = false;

	private long startTime;

	/**
	 * @return the startTime
	 */
	public long getStartTime() {
		return startTime;
	}

	private Highscore highscorelist = new Highscore(this);
	private int[] winRatio = new int[] { 0, 0 };

	public BattleshipGame() {
		RandomMapGenerator hrand = new RandomMapGenerator();
		RandomMapGenerator crand = new RandomMapGenerator();

		humanMap = hrand.createNewRandomMap();
		cpuMap = crand.createNewRandomMap();
		try {
			humanMap.localizeShips();
		} catch (BoatFailException e) {
			System.err.println("Failure in RandomMapGenerator. Wrong humanMap");
		} catch (NumberFailException e) {
			System.err.println("Failure in RandomMapGenerator. Wrong humanMap");
		}

		try {
			cpuMap.localizeShips();
		} catch (BoatFailException e) {
			System.err.println("Failure in RandomMapGenerator. Wrong cpuMap");
		} catch (NumberFailException e) {
			System.err.println("Failure in RandomMapGenerator. Wrong cpuMap");
		}
		ammo = 5;
		setAmmoforShips();

		try {
			loadHighscore();
		} catch (IOException e) {
			System.err.println("Unable to read Highscore.");
		}
	}

	public BattleshipGame(File levelFile) throws IOException,
			InvalidLevelFileException, BoatFailException, NumberFailException {
		this.readLvlFromFile(levelFile);
		humanMap.localizeShips();
		cpuMap.localizeShips();
		ammo = 1;
		setAmmoforShips();

		try {
			loadHighscore();
		} catch (IOException e) {
			System.err.println("Unable to read Highscore.");
		}
	}

	public BattleshipGame(SingleMap humanMap, SingleMap cpuMap)
			throws BoatFailException, NumberFailException {
		this.humanMap = humanMap;
		humanMap.localizeShips();
		this.cpuMap = cpuMap;
		cpuMap.localizeShips();
		ammo = 1;
		setAmmoforShips();

		try {
			loadHighscore();
		} catch (IOException e) {
			System.err.println("Unable to read Highscore.");
		}
	}

	public BattleshipGame(String levelString) throws InvalidLevelFileException,
			BoatFailException, NumberFailException {
		this.readLvlFromString(levelString);
		humanMap.localizeShips();
		cpuMap.localizeShips();
		ammo = 1;
		setAmmoforShips();

		try {
			loadHighscore();
		} catch (IOException e) {
			System.err.println("Unable to read Highscore.");
		}
	}
	
	
	/**
	 * Procedure for the CPU shooting. After the selected coordinates are hit,
	 * the informations about the shoot are stored in the eventList.
	 * 
	 * @throws InvalidCoordinatesException
	 * @throws FieldAlreadyShotException
	 */
	public void cpuShoot() throws InvalidCoordinatesException,
			FieldAlreadyShotException {
		if (turn == CPU && !isFinished()) {

			Coordinates target = cpuBot.getNewTarget(humanMap, cpuMap);
			if (ammunitionMode)
				cpuBot.CPUchooseShipToShoot();
			try {
				humanMap.shootAt(target);
			} catch (InvalidCoordinatesException e) {
				throw new InvalidCoordinatesException(
						"CPU failed to shoot at correct field");
			} catch (FieldAlreadyShotException e) {
				throw new FieldAlreadyShotException(
						"CPU Epic-failed choosing new field.");
			}

			if (humanMap.getEntry(target).isShip()) {
				cpuBot.notifyhit();
				Ship ship = humanMap.getShip(target);
				if (ship != null && ship.isSunk()) {
					cpuBot.notifyShipDown(ship);
				}
			}

			int time = (int) ((System.currentTimeMillis() - startTime) / 1000);

			eventList.addElement(new Event("CPU", turnNumber, target, time,
					startTime));
			tochoose = true;// für ammo
			turn = PLAYER;
		}

	}

	/**
	 * Creates random maps for the player and the CPU, while regarding the rules
	 * of placement.
	 * 
	 * @throws NumberFailException
	 * @throws BoatFailException
	 */
	public void createRandomLvl() throws BoatFailException, NumberFailException {
		reset();
		humanMap.generateRandomMap();
		cpuMap.generateRandomMap();
		humanMap.localizeShips();
		cpuMap.localizeShips();
		setAmmoforShips();

		turn = PLAYER;

	}

	public boolean isAmmunitionMode() {
		return ammunitionMode;
	}

	public void setAmmunitionMode(boolean ammunitionMode) {
		this.ammunitionMode = ammunitionMode;
	}

	public CPUnew getCpuBot() {
		return cpuBot;
	}

	public SingleMap getCpuMap() {
		return cpuMap;
	}

	public Vector<Event> getEventList() {
		return eventList;
	}

	public Highscore getHighscorelist() {
		return highscorelist;
	}

	public SingleMap getHumanMap() {
		return humanMap;
	}

	public String getPlayerName() {
		return playerName;
	}

	public int getShotsUsed() {
		return turnNumber;
	}

	public boolean getTurn() {
		return turn;
	}

	/**
	 * Tries to find the winner of the current Battleship Game.
	 * 
	 * @return -1 means game has not ended. <br>
	 *         0 means its a draw (only if cpu is allowed to shoot after Player
	 *         won. <br>
	 *         1 means player won. <br>
	 *         2 means cpu won.
	 */
	public int getWinner() {
		int winner, cpuLeft = cpuMap.countLeftParts(), humanLeft = humanMap
				.countLeftParts();

		winner = (cpuLeft == 0) ? (humanLeft == 0 ? 0 : 1)
				: (humanLeft == 0 ? 2 : -1);
		if (noAmmoLeft(humanMap)) {
			winner = 2;
		}
		if (noAmmoLeft(cpuMap)) {
			winner = 1;
		}
		return winner;
	}

	/**
	 * Checks if the Game has ended. A Game is finished when all ships on one of
	 * the maps are sunk.
	 * 
	 * @return true if game ended, otherwise false.
	 */
	public boolean isFinished() {

		// Falls Spiel erst seit diesem Aufruf beendet, schliesse BSG ab.
		if (!finished
				&& (humanMap.countLeftParts() == 0
						|| cpuMap.countLeftParts() == 0 || noAmmoLeft(cpuMap))
				|| noAmmoLeft(humanMap)) {
			finished = true;

			// User hat gewonnen
			if(getWinner() == 1){
				// Highscore erstellen
				if (getWinner() == 1) {
					setUserGotHighscore(highscorelist.addScoreToList());
					if (userGotHighscore)
						saveHighscore();
				}
				
			// Winratio erneuern
				winRatio[0]++;
			}
			if (getWinner() == 2) { // CPU hat gewonnen
				winRatio[1]++;
			}

		}

		return finished;
	}

	public int[] getWinRatio() {
		return winRatio;
	}

	public void setWinRatio(int[] winRatio) {
		this.winRatio = winRatio;
	}

	private void setUserGotHighscore(boolean b) {
		userGotHighscore = b; 
	}

	/**
	 * @return the userGotHighscore
	 */
	public boolean isUserGotHighscore() {
		if (isFinished())
			return userGotHighscore;
		return false;
	}

	/**
	 * Loads a random level from the level folder.
	 * 
	 * @throws NumberFailException
	 * @throws BoatFailException
	 * @throws IOException
	 * @throws InvalidLevelFileException
	 */
	public void loadRandomLvl() throws IOException, InvalidLevelFileException,
			BoatFailException, NumberFailException {
		reset();
		String lvlStr;
		Random rnd = new Random();
		// 19 mitgelieferte Lvl
		int lvls = 19;
		int takeLvl = rnd.nextInt(lvls) + 1;

		if (takeLvl < 10) {
			lvlStr = "src/de/tu_darmstadt/gdi1/resources/levels/defaultlevels/0"
					+ takeLvl + ".lvl";
		} else {
			lvlStr = "src/de/tu_darmstadt/gdi1/resources/levels/defaultlevels/"
					+ takeLvl + ".lvl";
		}
		File f = new File(lvlStr);

		readLvlFromFile(f);
		humanMap.localizeShips();
		cpuMap.localizeShips();
		setAmmoforShips();

		turn = PLAYER;

	}

	/**
	 * Procedure for the player shooting. After the selected coordinates are
	 * hit, the informations about the shoot are stored in the eventList.
	 * 
	 * @param target
	 *            the coordinates where the player's shoot is directed to.
	 * @throws NotUsersTurnException
	 * @throws InvalidCoordinatesException
	 * @throws FieldAlreadyShotException
	 */
	public void playerShoot(Coordinates target) throws NotUsersTurnException,
			InvalidCoordinatesException, FieldAlreadyShotException {
		if (turn == PLAYER && !isFinished()) {

			cpuMap.shootAt(target);
			turn = CPU;

		} else {
			if (!isFinished())
				throw new NotUsersTurnException("It's the oponents");
			else
				throw new NotUsersTurnException(
						"The Game has ended. Please restart.");
		}
		turnNumber++;

		if (eventList.isEmpty()) {

			startTime = System.currentTimeMillis();

		}
		int time = (int) ((System.currentTimeMillis() - startTime) / 1000);
		eventList.addElement(new Event(playerName, turnNumber, target, time,
				startTime));
	}

	/**
	 * reads a lvl string out of a file
	 * 
	 * @param lvlFile
	 *            the level's directory
	 * @throws IOException
	 * @throws InvalidLevelFileException
	 */
	public void readLvlFromFile(File lvlFile) throws IOException,
			InvalidLevelFileException {
		StringBuffer lvlString = new StringBuffer();

		// Place Reader on Levelfile
		FileReader lvlReader = new FileReader(lvlFile);

		// Read Levelfile char by char
		int c;
		while ((c = lvlReader.read()) != -1) {
			lvlString.append((char) c);
		}

		readLvlFromString(lvlString.toString());

	}

	/**
	 * imports level from the string into the game
	 * 
	 * @param lvl
	 *            the maps given as a string
	 * @throws InvalidLevelFileException
	 */
	public void readLvlFromString(String lvl) throws InvalidLevelFileException {

		// clear all data in BSG
		reset();

		// Define valid symbols
		Vector<Character> validSymbols = new Vector<Character>(13);
		validSymbols.add('l');
		validSymbols.add('r');
		validSymbols.add('t');
		validSymbols.add('b');
		validSymbols.add('v');
		validSymbols.add('h');
		validSymbols.add('L');
		validSymbols.add('R');
		validSymbols.add('T');
		validSymbols.add('B');
		validSymbols.add('V');
		validSymbols.add('H');
		validSymbols.add('-');
		validSymbols.add('*');
		validSymbols.add('|');
		validSymbols.add('\n');

		// Read Levelfile char by char
		int rc = 0, cc = 0, b = 0; // current Symbol, row counter, column
		// counter, boolean switch between human
		// and cpu map.
		for (char sym : lvl.toCharArray()) {
			// Throw InvalidLevelFileException if file is not formatted correct.
			if (validSymbols.contains(sym))// ??! && rc < 10 && cc < 20)
				switch (sym) {
				case '|':
					if (cc == 10) {
						b++;
						cc = 0;
					} else {
						throw new InvalidLevelFileException("");
					}
					break;
				case '\n':
					if (cc == 10) {
						cc = b = 0;
						rc++;
					} else {
						throw new InvalidLevelFileException("");
					}
					break;
				default:
					if (b == 0) {
						humanMap.setEntry(cc, rc, new ExtendedField(sym));
					}
					if (b == 1) {
						cpuMap.setEntry(cc, rc, new ExtendedField(sym));
					}
					cc++;
				}
			else {
				throw new InvalidLevelFileException("");
			}
		}

		// Check if Maps are valid & localize ships
		try {
			cpuMap.localizeShips();
			humanMap.localizeShips();
		} catch (BoatFailException e) {
			throw new InvalidLevelFileException(
					"Placement of ship parts does not make sense.");
		} catch (NumberFailException e) {
			throw new InvalidLevelFileException(
					"Wrong number of ships in level.");
		}

		// Erstellen des cpuBots
		cpuBot = new CPUnew(humanMap, cpuMap);
		cpuBot.notifyNewLevel();

		// Ammo setzen
		setAmmoforShips();

		// Zeit zurücksetzen
		startTime = 0;

	}

	/**
	 * Resets statistics of the last game such as the shot counter and the
	 * vector of left ship parts clears the maps and resets the cpu
	 */
	public void reset() {

		// clear Maps
		humanMap = new SingleMap();
		cpuMap = new SingleMap();

		// clear statistics and history
		turnNumber = 0;
		eventList.clear();
		finished = false;
		turn = PLAYER;

		// reset CPUPlayer
		// TODO: Anpassen fuer variable CPU
		cpuBot = new CPUnew();

	}

	public void setCpuBot(CPUnew cpuBot) {
		this.cpuBot = cpuBot;
	}

	public void setCpuMap(SingleMap cm) throws BoatFailException,
			NumberFailException {
		cpuMap = cm;
		cpuMap.localizeShips();
	}

	public void setHumanMap(SingleMap hm) throws BoatFailException,
			NumberFailException {
		humanMap = hm;
		humanMap.localizeShips();
	}

	public void setPlayerName(String playerName) {
		this.playerName = playerName;
		if (isUserGotHighscore()) {
			int rank = getHighscorelist().getScoreList().indexOf(
					getHighscorelist().getLastAdded());
			getHighscorelist().getScoreList().get(rank).setName(playerName);
			saveHighscore();
		}
	}

	public void setTurn(boolean turn) {
		this.turn = turn;
	}

	/**
	 * Generates string view of the map
	 * 
	 */
	@Override
	public String toString() {
		if (humanMap == null || cpuMap == null)
			return "";

		StringBuffer sb = new StringBuffer();

		for (int y = 0; y < 10; y++) {
			for (int x = 0; x < 10; x++) {
				sb.append(humanMap.getEntry(x, y).getSymbol());
			}
			sb.append('|');
			for (int x = 0; x < 10; x++) {
				sb.append(cpuMap.getEntry(x, y).getSymbol());
			}
			sb.append('\n');
		}
		return sb.toString();
	}

	/**
	 * generates String representation of current game status.
	 * 
	 * @return String game variables as String
	 */
	public String generateSaveString() {

		StringBuffer sb = new StringBuffer("###BSG\n"); // Init BattleshipGame
		sb.append(turn).append('\n'); // boolean count
		sb.append(turnNumber).append('\n'); // int Name
		sb.append(playerName).append('\n'); // String
		sb.append(ammo).append('\n');
		sb.append(ammunitionMode).append('\n');
		sb.append(System.currentTimeMillis() - startTime).append('\n'); // long
		// duration
		// History:
		sb.append(eventList.size()).append('\n'); // int length of eventList
		for (Event e : eventList) {
			sb.append(e.generateSaveString());
		}

		// Maps:
		sb.append(humanMap.generateSaveString());
		sb.append(cpuMap.generateSaveString());

		// CPUBot:
		sb.append(cpuBot.generateSaveString());

		System.out.println(sb.length());

		return sb.toString();
	}

	/**
	 * loads a bsg ,by reading out the saved string
	 * 
	 * @param br
	 *            BufferedReader
	 * @throws IOException
	 */

	public void loadSaveString(BufferedReader br) throws IOException {
		reset();

		if (br.readLine().equals("###BSG")) {
			turn = Boolean.valueOf(br.readLine());
			turnNumber = Integer.valueOf(br.readLine());
			playerName = br.readLine();
			ammo = Integer.valueOf(br.readLine());
			ammunitionMode = Boolean.valueOf(br.readLine());
			startTime = System.currentTimeMillis()
					- Long.valueOf(br.readLine());
			int length = Integer.valueOf(br.readLine());
			for (int i = 0; i < length; i++) {
				Event e = null;
				e = new Event("", 0, new Coordinates(), 0, 0);
				e.loadSaveString(br);
				eventList.add(e);
			}
			humanMap.loadSaveString(br);
			cpuMap.loadSaveString(br);
			cpuBot.loadSaveString(br);
		} else {
			System.err.println("Buffered Reader does not begin with ###BSG");
		}
	}

	private boolean tochoose = true;

	public boolean getTochoose() {
		return tochoose;
	}

	public void setTochoose(boolean tochoose) {
		this.tochoose = tochoose;
	}

	/**
	 * after a ship has been chosen to shoot from, lowers the ammunition on the
	 * ship part.
	 * 
	 * @param coords
	 *            coordinates of the ship part
	 * @throws NoAmmoException
	 */
	public void chooseShipToShoot(Coordinates coords) throws NoAmmoException {
		if (humanMap.getMap()[coords.getX()][coords.getY()].getSymbol() == 'r'
				|| humanMap.getMap()[coords.getX()][coords.getY()].getSymbol() == 'l'
				|| humanMap.getMap()[coords.getX()][coords.getY()].getSymbol() == 'b'
				|| humanMap.getMap()[coords.getX()][coords.getY()].getSymbol() == 't'
				|| humanMap.getMap()[coords.getX()][coords.getY()].getSymbol() == 'h'
				|| humanMap.getMap()[coords.getX()][coords.getY()].getSymbol() == 'v') {
			if (humanMap.getMap()[coords.getX()][coords.getY()].getAmmunition() == 0) {
				throw new NoAmmoException("Empty");
			} else {

				humanMap.getMap()[coords.getX()][coords.getY()]
						.setAmmunition(humanMap.getMap()[coords.getX()][coords
								.getY()].getAmmunition() - 1);
				tochoose = false;
				System.out.println(humanMap.getMap()[coords.getX()][coords
						.getY()].getAmmunition());
			}
		} else {
			throw new NoAmmoException("No ammo");
		}
	}

	/**
	 * sets the initial ammo on every ship part on the human and cpu map after
	 * the map is generated
	 */
	public void setAmmoforShips() {
		for (int i = 0; i < 10; i++) {
			for (int j = 0; j < 10; j++) {
				if (humanMap.getMap()[i][j].getSymbol() == 'v'
						|| humanMap.getMap()[i][j].getSymbol() == 't'
						|| humanMap.getMap()[i][j].getSymbol() == 'h'
						|| humanMap.getMap()[i][j].getSymbol() == 'b'
						|| humanMap.getMap()[i][j].getSymbol() == 'l'
						|| humanMap.getMap()[i][j].getSymbol() == 'r') {
					humanMap.getMap()[i][j].setAmmunition(ammo);
				}
				if (cpuMap.getMap()[i][j].getSymbol() == 'v'
						|| cpuMap.getMap()[i][j].getSymbol() == 't'
						|| cpuMap.getMap()[i][j].getSymbol() == 'h'
						|| cpuMap.getMap()[i][j].getSymbol() == 'b'
						|| cpuMap.getMap()[i][j].getSymbol() == 'l'
						|| cpuMap.getMap()[i][j].getSymbol() == 'r') {
					cpuMap.getMap()[i][j].setAmmunition(ammo);

				}

			}
		}
	}

	public boolean noAmmoLeft(SingleMap map) {
		if (ammunitionMode){
		for (int i = 0; i < 10; i++) {
			for (int j = 0; j < 10; j++) {
				if ((map.getMap()[i][j].getSymbol() == 'v'
						|| map.getMap()[i][j].getSymbol() == 't'
						|| map.getMap()[i][j].getSymbol() == 'h'
						|| map.getMap()[i][j].getSymbol() == 'b'
						|| map.getMap()[i][j].getSymbol() == 'l' || map
						.getMap()[i][j].getSymbol() == 'r')
						&& !(map.getMap()[i][j].getAmmunition() == 0)) {
					return false;
				}
			}
		}
		return true;}
		else return false;
	}

	/**
	 * 
	 * @return String representation of loaded Level with all hits removed.
	 */
	public String getVirginLevelString() {
		return toString().toLowerCase().replace('*', '-');
	}

	public void restartLevel() throws InvalidLevelFileException {
		try {
			readLvlFromString(getVirginLevelString());
		} catch (InvalidLevelFileException e) {
			throw new InvalidLevelFileException(
					"Could not restart Level: No valid level file loaded");
		}
	}

	public void saveHighscore() {
		File f = new File(
				"src/de/tu_darmstadt/gdi1/resources/highscore/save.bsh");
		try {
			if (!f.exists()) {
				f.createNewFile();
			}
			FileWriter fW = new FileWriter(f, false);
			BufferedWriter bW = new BufferedWriter(fW, 4);
			// StringReader sR = new StringReader(bsg.generateSaveString())
			// BufferedReader bR = new BufferedReader(sR);
			try {
				bW.write(getHighscorelist().generatesSaveString());
			} finally {
				bW.close();
			}

		} catch (IOException e) {
			System.out.println(Messages.getString("Info.noSave"));
		}	
	}

	/**
	 * Read Highscore from resources.
	 * 
	 * @throws IOException
	 *             thrown if resources/highscore/save.bsh does not exist or is
	 *             not readable for some reason.
	 */
	public void loadHighscore() throws IOException {
		FileReader fR = new FileReader(new File(
				"src/de/tu_darmstadt/gdi1/resources/highscore/save.bsh"));
		BufferedReader br = new BufferedReader(fR);
		getHighscorelist().loadSaveString(br);
		fR.close();
	}

	/**
	 * set method for the amount of ammunition per ship part
	 * 
	 * @param a
	 */
	public void setAmmunitionNumber(int a) {
		ammo = a;
	}

	/**
	 * 
	 * @return number of shots per ship part if in ammunition mode
	 * @see isAmmunitionMode()
	 */
	public int getAmmunitionNumber() {
		return ammo;
	}

}